{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center> Анализ объявлений и построение прогнозной модели посещаемости \n",
    "<center> Автор: Михаил Усков\n",
    "\n",
    "<center> На основе это проекта написана [статья](https://habrahabr.ru/post/280500/) на Хабре."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для выполнения проекта была поставлена задача построить модель, которая могла бы по тексту объявления в интернете спрогнозировать посещаемость данного объявления. Для получения данных был разработан краулер, который скачивал объявления с сайта и формировал итоговый csv-файл. Тематика объявлений была одинакова для всех - передача собак из приютов и от частных лиц в добрые руки. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Загрузка библиотек"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "import datetime\n",
    "\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "import seaborn\n",
    "\n",
    "# кириллица на графиках\n",
    "from matplotlib import rc\n",
    "font = {'family': 'Verdana',\n",
    "        'weight': 'normal'}\n",
    "rc('font', **font)\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "\n",
    "\n",
    "from pylab import rcParams\n",
    "rcParams['figure.figsize'] = 15, 10\n",
    "\n",
    "import pandas as pd\n",
    "pd.set_option('display.height', 1000)\n",
    "pd.set_option('display.max_rows', 500)\n",
    "pd.set_option('display.max_columns', 500)\n",
    "pd.set_option('display.width', 1000)\n",
    "pd.set_option('display.expand_frame_repr', True)\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "from sklearn.feature_extraction.text import CountVectorizer,TfidfTransformer\n",
    "\n",
    "from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier\n",
    "from sklearn.ensemble import GradientBoostingClassifier\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.cross_validation import cross_val_score, train_test_split\n",
    "from sklearn.decomposition import TruncatedSVD\n",
    "from sklearn.preprocessing import LabelEncoder\n",
    "from sklearn.linear_model import SGDClassifier\n",
    "from sklearn.grid_search import GridSearchCV\n",
    "from sklearn.metrics import classification_report, confusion_matrix, f1_score\n",
    "from sklearn.learning_curve import validation_curve,learning_curve\n",
    "\n",
    "from sklearn.base import TransformerMixin\n",
    "\n",
    "from sklearn.feature_selection import SelectKBest, chi2\n",
    "\n",
    "\n",
    "\n",
    "from nltk.corpus import stopwords\n",
    "from nltk.stem.snowball import RussianStemmer\n",
    "import Stemmer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Описание данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В исходных данных содержится название, описание и общее число посещений объявления с начала публикации."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Список переменных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. description - полный текст объявления\n",
    "+ identificator - номер объявления на сайте\n",
    "+ num_counts - число посещений объявления с начала его размещения\n",
    "+ price - цена, за которую предлагается купить животное. обычно, волонтеры ставят 100р. или вовсе не указывают цену.\n",
    "+ start_date - дата, когда объявление было размещено\n",
    "+ title - название объявления, как оно выглядит на первой странице."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset = pd.read_csv('../../data/billdoard_dataset.csv', header = 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Описательные статистики"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# посещаемость\n",
    "dataset['num_counts'].hist()\n",
    "plt.title('Number of views')\n",
    "plt.show()\n",
    "\n",
    "# логарифм посещаемости\n",
    "dataset['num_counts'].apply(np.log).hist()\n",
    "plt.title('Log of Number of views')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# цена\n",
    "print(pd.value_counts(dataset['price']).head(20))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Число слов в объявлении:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_of_words = dataset['description'].apply(lambda x: len(x.split(' ')))\n",
    "plt.hist(num_of_words)\n",
    "plt.title('Distribution of the number of words')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Диаграммы рассеяния будут построены после предобработки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Предобработка\n",
    "\n",
    "1. Признаками будут являться отдельные слова или биграммы, поэтому их необходимо нормализовать, чтобы одно и то же слово, но в разных формах не считалось как два разных. Эта операция называется стемминг, что согласно вики значит процесс нахождения основы слова для заданного исходного слова. \n",
    "2. Поле \"date\" содержит дату в форме строки, поэтому ее нужно перевести в правильный формат\n",
    "3. В качестве признака берется поле \"description\", поэтому текст необходимо перевести в представление \"bag of words\" и применить tf-idf. При этом из текста убираются предлоги, вспомогательные частицы и т.д. Эти слова называются stopwords.\n",
    "4. Поскольку каждое объявление имеет свою дату публикации, то целевая переменная здесь представляет не просто число просмотров, а число просмотров, разделенное на число дней с момента подачи до скачивания их моим краулером. Т.е. можно считать, что это грубая оценка числа посетителей в сутки.\n",
    "5. После нескольких неудачных попыток восстановить регрессию между document-term matrix и средним числом посетителей, было принято решение разбить целевую переменную на интервалы(квартили) и рассматривать задачу классификации (отсюда и tf-idf). Т.е. на выходе модель будет прогнозировать интервал, где содержится средняя посещаемость для данного объявления. Преобразование в квартили производилось только на обучающей выборке, поэтоу необходимо написать функцию, которая преобразовает и тестовую. Преобразовывать всю выборку целиком нельзя, посольку тогда тестовые данные будут косвенно участвовать в обучении.\n",
    "6. Поле 'price' представляет собой цену за животное. Большие цены являются индикатором продажи породистого животного, нас же интересует некоммерческая деятельность, поэтому оставляем только те записи, для которых price < 500р.\n",
    "7. Разбиение на train\\test. Причем на train будет проводится обучение и подбор параметров по сетке на кросс-валидации, а на test будет проверяться финальное качество. Основная метрика - accuracy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# стемминг\n",
    "stop = stopwords.words('russian')\n",
    "stemobject = Stemmer.Stemmer('russian')#RussianStemmer()\n",
    "def stemmer(x):\n",
    "    stem_function = stemobject.stemWord\n",
    "    out = [stem_function(word) for word in x.split(' ')]\n",
    "    return ' '.join(out)\n",
    "\n",
    "# преобразование тестовых данных в квартили\n",
    "def y_transform(x):\n",
    "    \n",
    "    if isinstance(x,str):\n",
    "        return x\n",
    "    \n",
    "    categories = y_train.unique().categories.get_values()\n",
    "    \n",
    "    first = categories[0]\n",
    "    first = first.replace('[','').replace(']','').split(',')\n",
    "    first = [float(i) for i in first]\n",
    "\n",
    "    last = categories[-1]\n",
    "    last = last.replace('(','').replace(']','').split(',')\n",
    "    last = [float(i) for i in last]\n",
    "    \n",
    "    if (x < first[0]):\n",
    "        return categories[0] \n",
    "        \n",
    "    if (x >= first[0]) & (x <= first[1]):\n",
    "        return categories[0]\n",
    "    \n",
    "    if (x >= last[0]) & (x <= last[1]):\n",
    "        return categories[-1] \n",
    "    \n",
    "    if (x >= last[1]):\n",
    "        return categories[-1]  \n",
    "        \n",
    "    for cat in categories[1:-1]:\n",
    "        c = cat.replace('(','').replace(']','').split(',')\n",
    "        c = [float(i) for i in c]\n",
    "        if (x > c[0]) & (x <= c[1]):\n",
    "            return cat\n",
    "        \n",
    "# обработка даты\n",
    "month = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря']\n",
    "order = {M: i+1 for i,M in enumerate(month)}\n",
    "\n",
    "def date_helper(x):\n",
    "    now = datetime.datetime(2016,3,18)\n",
    "    x = x.split(' ')\n",
    "    d = x[0]\n",
    "    m = order[x[1]]\n",
    "    y = x[2]\n",
    "    \n",
    "    dateobject =  datetime.datetime(int(y),int(m),int(d))\n",
    "    delta = now-dateobject\n",
    "    return delta.days"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# идентификатор не несет полезной нагрузки\n",
    "dataset.drop(['identificator'], axis = 1, inplace = True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset['num_days'] = dataset['start_date'].apply(date_helper)\n",
    "dataset.drop(['start_date'], axis = 1, inplace = True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Создание новой целевой переменной как отношения общего числа посещений к числу дней, прошедших с момента публикации до момента попадания объявления в выборку. В знаменателе добавляет единичка, чтобы избежать деления на нуль."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset['mean_count'] = dataset['num_counts'] / (dataset['num_days']+1)\n",
    "dataset.drop(['num_counts'], axis = 1, inplace = True)\n",
    "dataset.drop(['num_days'], axis = 1, inplace = True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ниже на графике видно, что чем меньше слов в объявлении, тем больше просмотров, если оценивать грубо. Предполагаю, что это справедливо только для объявлений данной конкретной тематики, поскольку чем больше слов, тем скорее всего сложнее история у животного."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.scatter(num_of_words,dataset['mean_count'])\n",
    "plt.xlabel('Number of words')\n",
    "plt.ylabel('Mean Number of visits')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Рассмотрим те объявления, которые составляют топ-20 по среднему числу посещений:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sorted_df = dataset.sort(['mean_count'], ascending=False)\n",
    "for i in (sorted_df.index.tolist()[:10]):\n",
    "    print('==================')\n",
    "    print('\\nНазвание: ')\n",
    "    print('----------')\n",
    "    print(sorted_df.loc[i,'title'])\n",
    "    \n",
    "    print('\\nОписание: ')\n",
    "    print('----------')\n",
    "    print(sorted_df.loc[i,'description'])\n",
    "\n",
    "    \n",
    "    print('\\nСреднее число просмотров: ',sorted_df.loc[i,'mean_count'])\n",
    "    print('--------------------------')\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что наибольшей популярностью пользуются попродистые собаки. Теперь рассмотрим наименее популярные объявления:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sorted_df = dataset.sort(['mean_count'], ascending=True)\n",
    "for i in (sorted_df.index.tolist()[:10]):\n",
    "    print('==================')\n",
    "    print('\\nНазвание: ')\n",
    "    print('----------')\n",
    "    print(sorted_df.loc[i,'title'])\n",
    "    \n",
    "    print('\\nОписание: ')\n",
    "    print('----------')\n",
    "    print(sorted_df.loc[i,'description'])\n",
    "\n",
    "    \n",
    "    print('\\nСреднее число просмотров: ',sorted_df.loc[i,'mean_count'])\n",
    "    print('--------------------------')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "К сожалению, животные из приютов пользуются меньшей популярностью."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.hist(np.log(dataset['mean_count']))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# если цена указанна как 'Неуказана', 'Договорная' то она заменяется на 100р.\n",
    "dataset['price'] = dataset['price'].apply(lambda x: 100 if x in ['Неуказана', 'Договорная'] else int(x))\n",
    "dataset = dataset[dataset['price'] <= 500]\n",
    "dataset.drop(['price'], axis = 1, inplace = True)\n",
    "dataset.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Стемминг можно применить ко всему набору сразу, поскольку для работы он использует только данное конкретное слово"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset['description'] = dataset['description'].apply(stemmer) "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_test, y_train, y_test  = train_test_split(dataset['description'],dataset['mean_count'], test_size=0.33)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разбиение целевой переменной на 5 квартилей. Т.е. получили задачу многоклассовой классификации из 5 классов"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_train = pd.qcut(y_train, q= 5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Рассмотрим baseline - qcut разбил выборку равномерно - по 454 объекта в каждом квартиле. Если всем объектам приписать какой-нибудь один класс, то качество будет 20%."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(pd.value_counts(y_train))\n",
    "print(pd.value_counts(y_train))\n",
    "print('Baseline accuracy: ',454/y_train.shape[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сетка для поиска гиперпараметров.Поскольку интересно знать, какие слова влияют на посещаеомсть, было решенно не использовать LSA"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "parameters = {\n",
    "    'vect__max_df': (0.5, 0.75,1),\n",
    "    #'vect__max_features': (10000, 50000),\n",
    "    #'vect__ngram_range': ((1, 2)),  # unigrams or bigrams\n",
    "    #'tfidf__use_idf': (True,False),\n",
    "    #'tfidf__norm': ('l2'),\n",
    "    #'svd__n_components':(50,100,1000),\n",
    "    \"clf__max_depth\": [3, None],\n",
    "    \"clf__max_features\": [1, 3, 10],\n",
    "    \"clf__min_samples_split\": [1, 3, 10],\n",
    "    \"clf__min_samples_leaf\": [1, 3, 10],\n",
    "    \"clf__bootstrap\": [True, False]\n",
    "}\n",
    "\n",
    "total_comb = 1\n",
    "for i in parameters.values():\n",
    "    total_comb *= len(i)\n",
    "print('Всего комбинаций: ', total_comb)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве классификатора - случайный лес."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pipe = Pipeline([('vect', CountVectorizer(stop_words=stop, ngram_range=(1, 2), max_features=10000)),\n",
    "                 ('tfidf', TfidfTransformer(use_idf=True,norm='l2')),\n",
    "                 #('svd', TruncatedSVD(algorithm='randomized')),\n",
    "                 ('clf', RandomForestClassifier(n_estimators=500, verbose = False))]) \n",
    "\n",
    "\n",
    "grid_search = GridSearchCV(pipe, parameters, verbose=True, n_jobs = -1, cv = 5)\n",
    "grid_search.fit(X_train, y_train)\n",
    "\n",
    "print(\"Best score: %0.3f\" % grid_search.best_score_)\n",
    "print(\"Best parameters set:\")\n",
    "best_parameters = grid_search.best_estimator_.get_params()\n",
    "for param_name in sorted(parameters.keys()):\n",
    "    print(\"\\t%s: %r\" % (param_name, best_parameters[param_name]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Преобразование тестовой выборки в квартили\n",
    "y_test = y_test.apply(y_transform)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(classification_report(y_test,grid_search.predict(X_test)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(pd.crosstab(y_test,grid_search.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Топ-10 слов, которые сильнее всего влияют на классификацию"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_weights = {'name': grid_search.best_estimator_.named_steps['vect'].get_feature_names(),\n",
    "                  'weight': grid_search.best_estimator_.named_steps['clf'].feature_importances_}\n",
    "\n",
    "feature_weights = pd.DataFrame(data=feature_weights['weight'], index = feature_weights['name'], columns = ['weight'])\n",
    "feature_weights.sort(['weight'], ascending=False, inplace=True)\n",
    "feature_weights.head(10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots()\n",
    "feature_weights.head(50).plot(ax=ax,kind = 'bar')\n",
    "fig.autofmt_xdate()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Диаграммы рассеяния(scatterplots)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Вообще, этот этап должен быть в начале исследования, но поскольку у нас текст и сильно разреженная матрица объекты-признаки, то имеет смысл построить графики только для топ-7, в соответствии с информацией о важности признаков, полученной от случайного леса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import LabelEncoder\n",
    "\n",
    "dataset_tfidf = pd.DataFrame(grid_search.best_estimator_.named_steps['vect'].transform(X_train).toarray())\n",
    "dataset_tfidf.columns = grid_search.best_estimator_.named_steps['vect'].get_feature_names()\n",
    "dataset_tfidf = dataset_tfidf[feature_weights.index.tolist()[:7]]\n",
    "\n",
    "# поскольку у нас дискретные отсчеты, немного \"размоем\" значения, чтобы точки не ложились одна на другую:\n",
    "for k in dataset_tfidf.keys():\n",
    "    dataset_tfidf[k] = dataset_tfidf[k].apply(lambda x: int(x)+np.random.normal(0,0.08))\n",
    "\n",
    "dataset_tfidf['target'] =(y_train)\n",
    "\n",
    "seaborn.pairplot(dataset_tfidf, hue = 'target')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что желтые точки(интервал с максимальным числом просмотров) хорошо различаются от остальных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Кривые обучения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_with_std(x, data, **kwargs):\n",
    "        mu, std = data.mean(1), data.std(1)\n",
    "        lines = plt.plot(x, mu, '-', **kwargs)\n",
    "        plt.fill_between(x, mu - std, mu + std, edgecolor='none',\n",
    "                         facecolor=lines[0].get_color(), alpha=0.2)\n",
    "        \n",
    "def plot_learning_curve(clf, X, y, scoring, cv=5):\n",
    " \n",
    "    train_sizes = np.linspace(0.05, 1, 20)\n",
    "    n_train, val_train, val_test = learning_curve(clf,\n",
    "                                                  X, y, train_sizes, cv=cv,\n",
    "                                                  scoring=scoring, n_jobs = -1)\n",
    "    plot_with_std(n_train, val_train, label='training scores', c='green')\n",
    "    plot_with_std(n_train, val_test, label='validation scores', c='red')\n",
    "    plt.xlabel('Training Set Size'); plt.ylabel(scoring)\n",
    "    plt.legend()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_learning_curve(grid_search.best_estimator_,X_train, y_train, scoring='f1_weighted', cv=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Примечание:** иногда при разных запусках на валидационной кривой можно увидеть, что классификатор всегда отлично описывает обучающую выборку, но зазор между train и cv кривыми довльно большой. Скорее всего это индикатор переобучения и вот откуда оно взялось: в качестве классификатора передается pipeline, **гиперпараметры** которого были настроены ранее по той же обучающей выборке. Т.е. learning_curve конечно проводит обучение заново, но только для параметров случайного леса, а гиперпараметры остаются прежними. По хорошему, необходимо в learning_curve передать объект grid_search и для каждого размера обучающей выборки производить поиск по сетке, но это будет слишком долго для данной учебной задачи."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Построена модель прогнозирования среднего числа посещений объявления с качеством accuracy = 0.386, что почти в два раза больше, чем прогнозирование константным значением (accuracy = 0.20)\n",
    "2. Точнее всего модель предсказывает значения, которые находятся в крайних интервалах - [0.0888, 1.222] и (13.193, 324] \n",
    "3. Судя по значимости слов, люди обращают внимание на пол, возраст, медицинские и физиологические характеристики, умение гулять на поводке\n",
    "4. Качество модели может быть улучшено, если к тексту добавить фотографии. Для извлечения признаков из фото можно попробовать использовать сверточные нейронные сети, например, AlexNet"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Сылки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "1. [Text classification for russian language][1]\n",
    "+ [Russian stemming algorithm][2]\n",
    "+ [nltk.stem.snowball.RussianStemmer][3]\n",
    "+ [Classification of text documents using sparse features][4]\n",
    "\n",
    "\n",
    "[1]: http://stackoverflow.com/questions/18011756/text-classification-for-russian-language\n",
    "[2]: http://snowball.tartarus.org/algorithms/russian/stemmer.html\n",
    "[3]: http://www.nltk.org/api/nltk.stem.html\n",
    "[4]: http://scikit-learn.org/stable/auto_examples/text/document_classification_20newsgroups.html"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
